package edu.spbsu.nonsm00th.qdiff.parser;

import edu.spbsu.nonsm00th.qdiff.parser.exception.ArgumentsCountException;
import edu.spbsu.nonsm00th.qdiff.parser.exception.BracketsConsistentException;
import edu.spbsu.nonsm00th.qdiff.parser.exception.DelimeterMissedException;
import edu.spbsu.nonsm00th.qdiff.parser.exception.EmptyExpressionException;
import edu.spbsu.nonsm00th.qdiff.parser.exception.OpenBracketMissedException;
import edu.spbsu.nonsm00th.qdiff.parser.exception.ParserException;
import edu.spbsu.nonsm00th.qdiff.parser.exception.WrongExpressionException;
import edu.spbsu.nonsm00th.qdiff.parser.word.Argument;
import edu.spbsu.nonsm00th.qdiff.parser.word.Number;
import edu.spbsu.nonsm00th.qdiff.parser.word.Control;
import edu.spbsu.nonsm00th.qdiff.parser.word.Function;
import edu.spbsu.nonsm00th.qdiff.parser.word.Word;
import edu.spbsu.nonsm00th.qdiff.parser.word.Operation;
import edu.spbsu.nonsm00th.qdiff.parser.word.Unit;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

/**
 * Class, which translates expression from Infix notation (3+4) to Reverse Polish notation - RPN (34+).
 *
 *
 * @author mbuzdov
 */
public class RPNTranslator {

    private Stack<Unit> temp;
    private List<Unit> out;

    /**
     * Constructor for RPNTranslator.
     * @param expression the expression in inner notation represented by String.
     */
    public RPNTranslator() {
    }

    /**
     * Translates expression in inner notation to RPN.
     *
     * @param expression the expression represented by String
     * @return RPN representation of expression
     * @throws ParserException
     */
    public List<Unit> translate(String expression) throws ParserException {
        temp = new Stack<Unit>();
        out = new ArrayList<Unit>();
        ExpressionParser parser = new ExpressionParser();
        List<Unit> inner = parser.parse(expression);

        Stack<Level> levels = new Stack<Level>();
        levels.add(new Level());
        boolean isUnary = true; // for +, -
        boolean needOpen = false;
        boolean needExpression = true;
        boolean needDelimeter = false;
        for (Unit next : inner) {
            Word wnext = next.getWord();
            if (needOpen &&
                    !(wnext == Control.OPEN_BRACKET)) {
                throw new OpenBracketMissedException(next.getStartPosition());
            }
            if (needExpression &&//TODO: here throws exception if '()' - rewrite one of two
                    !(wnext == Control.OPEN_BRACKET || wnext instanceof Number ||
                    wnext instanceof Argument || wnext instanceof Function ||
                    (isUnary && (wnext == Operation.ADDITION || wnext == Operation.SUBTRACTION)))) {
                throw new WrongExpressionException(next.getStartPosition());
                //TODO:customize exceptions
            }
            if (needDelimeter &&
                    !(wnext instanceof Operation || Control.SEPARATOR == wnext || Control.CLOSE_BRACKET == wnext)) {
                throw new DelimeterMissedException(next.getStartPosition());
            }
            if (Control.CLOSE_BRACKET != wnext && Control.SEPARATOR != wnext) {//TODO: calls each time, add !current.isEmpty() or rewrite
                // levels always contains element
                Level current = levels.peek();
                current.fill();
            }

            // +, -, max, min - need to set arguments
            // after any FUNCTION need OPEN_BRACKET
            if (wnext instanceof Number || wnext instanceof Argument) {
                out.add(next);
            }
            if (wnext instanceof Function) {
                temp.push(next);
            }
            if (wnext instanceof Operation) {
                if (Operation.ADDITION == wnext || Operation.SUBTRACTION == wnext) {
                    next.setArgumentsCount(isUnary ? 1 : 2);
                }
                throwLowerPriorityOperations(((Operation)wnext).getPriority());
                temp.push(next);
            }

            if (Control.OPEN_BRACKET == wnext) {
                Level nlevel = new Level();
                levels.add(nlevel);
                temp.push(next);
            }
            if (Control.SEPARATOR == wnext) { // TODO: it may be better to verify arguments count here
                try {
                    throwTillOpenBracket();
                } catch(ParserException e) {
                    throw new BracketsConsistentException("There is no open bracket before separator.", next.getStartPosition());
                }
                Level current = levels.peek();
                if (current.isEmpty()) {
                    throw new EmptyExpressionException(next.getStartPosition());
                }
                current.separate();
            }
            if (Control.CLOSE_BRACKET == wnext) {
                try {
                    throwTillOpenBracket();
                } catch(ParserException e) {
                    throw new BracketsConsistentException("There is no open bracket related to close bracket.", next.getStartPosition());
                }
                Level current = levels.pop();
                if (current.isEmpty()) {
                    throw new EmptyExpressionException(next.getStartPosition());
                }
                current.close();
                int count = current.getCount();

                Unit openBracket = temp.pop(); // it is Control.OPEN_BRACKET
                if (!temp.isEmpty() && temp.peek().getWord() instanceof Function) {
                    Unit funit = temp.pop();
                    Function function = (Function)funit.getWord();
                    if (Function.LOG == function || Function.POWER == function) {
                        if (2 != count) {
                            throw new ArgumentsCountException("LOG and POWER functions should have 2 arguments.", funit.getStartPosition());
                        }
                    } else if (Function.MAX != function && Function.MIN != function) {
                        if (count != 1) {
                            throw new ArgumentsCountException("All functions, except LOG, POWER, MIN and MAX should have 1 argument.", funit.getStartPosition());
                        }
                    } else {
                        funit.setArgumentsCount(count);
                    }
                    out.add(funit);
                } else if (1 != count) {
                    throw new ArgumentsCountException("Two subexpressions can't be separated by delimeter.", openBracket.getStartPosition());
                }
            }

            isUnary = Control.OPEN_BRACKET == wnext || Control.SEPARATOR == wnext;
            needOpen = wnext instanceof Function;
            needExpression = wnext instanceof Operation || Control.OPEN_BRACKET == wnext || Control.SEPARATOR == wnext;
            needDelimeter = wnext instanceof Number || wnext instanceof Argument || Control.CLOSE_BRACKET == wnext;
        }
        while (!temp.isEmpty()) {
            Unit next = temp.pop();
            if (next.getWord() instanceof Operation) {
                out.add(next);
            } else {
                throw new BracketsConsistentException("There is missed close bracket for currently open bracket.", next.getStartPosition());
            }
        }
        return out;
    }


    /**
     * Should be used when we meet Control.SEPARATOR or Control.CLOSE_BRACKET.
     * Puts all words from temp to out till we meet Control.OPEN_BRACKET.
     * temp should contain Control.OPEN_BRACKET, because in other way it will be not correct expression.
     *
     * @throws ParserException
     */
    private void throwTillOpenBracket() throws ParserException {
        // if temp contains any Function it also contains Control.OPEN_BRACKET before.
        do {
            if (temp.isEmpty()) {
                throw new ParserException(-1);
            }
            if (Control.OPEN_BRACKET == temp.peek().getWord()) {
                return;
            }
            out.add(temp.pop());
        } while (true);
    }

    /**
     * Should be used when we meet any Operation.
     * Puts all operations with lower priority from temp to out till we meet Control.OPEN_BRACKET
     * or Operation with not lower priority.
     * 
     * @param priority the priority of Operation we met.
     */
    private void throwLowerPriorityOperations(int priority) {//TODO: verify! is it correct name or > need
        // if temp contains any Function it also contains Control.OPEN_BRACKET before.
        do {
            if (temp.isEmpty()) {
                return;
            }
            Word next = temp.peek().getWord();
            if (Control.OPEN_BRACKET == next ||
                    ((Operation)next).getPriority() < priority) {
                return;
            }
            out.add(temp.pop());
        } while (true);
    }

    /**
     * Private class for keeping (sub)expression data.
     * Subexpression - is a part between Control.Open_BRACKET/Control.SEPARATOR and Control.CLOSE_BRACKET/Control.SEPARATOR.
     */
    private class Level {

        /**
         * Number of arguments of (sub)expression.
         * Examples:
         * "max(1;3-x2;3+x1)" - 3 arguments
         * "power(1+x2;5+x1)" - 2 arguments
         */
        private int count;
        /**
         * Boolean variable, which indicates is (sub)expression is empty.
         * Next expressions contain empty (sub)expression: "max()", "max(2+x1;;3-x2)"
         */
        private boolean empty;

        /** Constructor method. */
        public Level() {
            count = 0;
            empty = true;
        }

        /** Should be call, when Control.CLOSE_BRACKET met. */
        public void close() {
            count++;
        }
        /** Should be call, when Control.SEPARATOR met. */
        public void separate() {
            count++;
            empty = true;
        }
        /** Should be call for marking (sub)expression as non empty. */
        public void fill() {
            empty = false;
        }

        /** Getter for count. */
        public int getCount() {
            return count;
        }
        /** Getter for empty. */
        public boolean isEmpty() {
            return empty;
        }
    }

}
